/**
 * Octopus, an XMMS2 client.
 * Copyright (C) 2014 Octopus Team
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * 
 * ---------------------------------------------------------------------
 * Requires properties visible_width and adjustment to be set when MxGrid
 * is inside a scrollable container.
 * 
 * Calculation values are documented in /doc/octopus-mx-grid.png 
 * 
 */

namespace OctopusMx {
	public class MxGrid : Mx.Widget, Clutter.Container {
		public float column_spacing { get; set; }
		public float row_spacing { get; set; }

		//visible_width and adjustment describe the visible part of this layout
		//visible part [from, to]: [adjustment, adjustment+visible_widht]
		private float _visible_width = 0;
		public float visible_width {
			get {	return _visible_width;
				}
			set {	_visible_width = value;
					visible_center = _adjustment + _visible_width/2;
					radius = _visible_width*2;
					z_diff = Math.sqrt (radius*radius - (_visible_width/2)*(_visible_width/2));
					this.queue_relayout ();
				}
		 }
		private float _adjustment = 0;
		public float adjustment {
			get {	return _adjustment;
				}
			set {	_adjustment = value;
					visible_center = _adjustment + (_visible_width)/2;
				}
		}

		//private
		private double visible_center;
		private double radius;
		private double z_diff;

		private Gee.HashMap<Clutter.Actor, MxGridActorData> hash_table;

		public MxGrid () {
			Object();
		}

		construct {
			hash_table = new Gee.HashMap<Clutter.Actor, MxGridActorData> ();
		}

		//Clutter.Actor
		public override void paint () {
			float x = 0;
			//float y = 0;
			Clutter.ActorBox grid_box;

			base.paint ();

			grid_box = get_allocation_box ();
			grid_box.x1 = x;
			grid_box.x2 = (grid_box.x2 - grid_box.x1) + x;
			//grid_box.y1 = y;
			//grid_box.y2 = (grid_box.y2 - grid_box.y1) + y;

			for (int i=0; i < get_n_children (); i++) {
				unowned Clutter.Actor child = get_child_at_index (i);
				//GLib.assert (child != null);

				Clutter.ActorBox child_box = child.get_allocation_box ();
				if (   (child_box.x1 < grid_box.x2)
				    && (child_box.x2 > grid_box.x1)
				//    && (child_box.y1 < grid_box.y2)
				//    && (child_box.y2 > grid_box.y1)
				    && child.visible) {
					child.paint ();
				}
			}
		}

		public override void pick (Clutter.Color color) {
			float x = 0;
			//float y = 0;
			Clutter.ActorBox grid_box;

			base.pick (color);

			grid_box = get_allocation_box ();
			grid_box.x1 = x;
			grid_box.x2 = (grid_box.x2 - grid_box.x1) + x;
			//grid_box.y1 = y;
			//grid_box.y2 = (grid_box.y2 - grid_box.y1) + y;

			for (int i=0; i < get_n_children (); i++) {
				unowned Clutter.Actor child = get_child_at_index (i);
				//GLib.assert (child != null);

				Clutter.ActorBox child_box = child.get_allocation_box ();
				if (   (child_box.x1 < grid_box.x2)
				    && (child_box.x2 > grid_box.x1)
				//    && (child_box.y1 < grid_box.y2)
				//    && (child_box.y2 > grid_box.y1)
				    && child.visible) {
					//fix child, according to deformation, it is not pixel perfect
					if (child is IMxGridActor) {
						child.z_position = ((IMxGridActor)child).stored_z_position;
						child.rotation_angle_y = ((IMxGridActor)child).stored_rotation_angle_y;
					} else {
						MxGridActorData data = hash_table.get (child);
						child.z_position = data.z_position;
						child.rotation_angle_y = data.rotation_angle_y;
					}
					child.paint ();
					child.z_position = 0;
					child.rotation_angle_y = 0;
					
				}
			}
		}

		public override void get_preferred_width (float for_height, out float min_width_p, out float natural_width_p) {
			float actual_width, min_width;
			Clutter.ActorBox box =  Clutter.ActorBox ();

			box.x1 = 0;
			box.y1 = 0;
			box.x2 = float.MAX;
			box.y2 = for_height;

			do_allocate (box, Clutter.AllocationFlags.ALLOCATION_NONE, true, out actual_width, null, out min_width, null);

			min_width_p = min_width;
			natural_width_p = actual_width;
		}

		public override void get_preferred_height (float for_width, out float min_height_p, out float natural_height_p) {
			float actual_height, min_height;
			Clutter.ActorBox box = Clutter.ActorBox ();

			box.x1 = 0;
			box.y1 = 0;
			box.x2 = for_width;
			box.y2 = float.MAX;

			do_allocate (box, Clutter.AllocationFlags.ALLOCATION_NONE, true, null, out actual_height, null, out min_height);

			min_height_p = min_height;
			natural_height_p = actual_height;
		}

		public override void allocate (Clutter.ActorBox box, Clutter.AllocationFlags flags) {
			base.allocate (box, flags);
			do_allocate (box, flags, false, null, null, null, null);
		}

		//Clutter.Container
		public void actor_added (Clutter.Actor actor) {
			if (!(actor is IMxGridActor)) {
				MxGridActorData data = new MxGridActorData ();
				hash_table.set (actor, data);
			}
		}

		public void actor_removed (Clutter.Actor actor) {
			if (!(actor is IMxGridActor))
				hash_table.unset (actor);
		}

		public int get_rows_from_target_height (float target_height, float album_size) {
			float padding_top = get_padding_top ();
			float padding_bottom = get_padding_bottom ();
			float rows = (target_height - padding_top - padding_bottom) /  (row_spacing + album_size);
			rows = GLib.Math.roundf (rows);
			return (int) rows;
		}

		//internal functions
		private void do_allocate (Clutter.ActorBox box, Clutter.AllocationFlags flags,
		                          bool      calculate_extents_only,
		                          out float actual_width,
		                          out float actual_height,
		                          out float min_width,
		                          out float min_height) {
			actual_width = 0;
			actual_height = 0;
			min_width = 0;
			min_height = 0;

			//grid
			float padding_top = get_padding_top ();
			float padding_right = get_padding_right ();
			float padding_bottom = get_padding_bottom ();
			float padding_left = get_padding_left ();
			float current_x = translate_width (0, padding_left);
			int   max_rows = 0;
			int   current_row = 0;

			//child
			float child_min_width = 0;
			float child_min_height = 0;
			float child_natural_width = 0;
			float child_natural_height = 0;
			float child_min_x2 = 0;
			float child_x2 = 0;
			float z_position = 0;
			float rotation_angle_y = 0;

			for (int i=0; i < get_n_children (); i++) {
				unowned Clutter.Actor child = get_child_at_index (i);
				//assume all children are same size, get size from fist child
				if (i==0) {
					child.get_preferred_size (out child_min_width, out child_min_height,
					                          out child_natural_width, out child_natural_height);
					
					max_rows = (int)(  (box.y2 - box.y1 - padding_top - padding_bottom) //available height
					                 / (row_spacing + child_natural_height)); //child height;
					if (max_rows == 0) max_rows = 1;

					translate_child (0, child.width, out z_position, out rotation_angle_y);
					child_min_x2 = translate_width (0, child_min_width);
					child_x2 = translate_width (0, child_natural_width);
				}
				if (!child.visible) continue;
				//if the child is overflowing we wrap to next line
				if (current_row == max_rows ) {
					//add child_natural_width
					current_x = current_x + translate_width (current_x, child_natural_width);
					//add column_spacing
					current_x = current_x + translate_width (current_x, column_spacing);
					translate_child (current_x, child.width, out z_position, out rotation_angle_y);
					child_min_x2 = translate_width (current_x, child_min_width);
					child_x2 = translate_width (current_x, child_natural_width);
					current_row = 0;
				}
				//set child allocation
				Clutter.ActorBox min_child_box = Clutter.ActorBox ();
				min_child_box.x1 = 0;
				min_child_box.y1 = 0;
				min_child_box.x2 = child_min_x2;
				min_child_box.y2 = child_min_height;
				Clutter.ActorBox child_box = Clutter.ActorBox ();
				child_box.x1 = current_x;
				child_box.y1 = padding_top + (current_row*(child_natural_height + row_spacing));
				child_box.x2 = child_box.x1 + child_x2;
				child_box.y2 = child_box.y1 + child_natural_height;
				if (!calculate_extents_only)
					child.allocate (child_box, flags);

				if ((child_box.x2 + padding_right) > actual_width)
					actual_width = child_box.x2 + padding_right;
				if ((child_box.y2 + padding_bottom) > actual_height)
					actual_height = child_box.y2 + padding_bottom;
				if ((padding_left + child_min_width + padding_right) > min_width)
					min_width = padding_left + child_min_width + padding_right;
				if ((padding_top + child_min_height + padding_bottom) > min_height)
					min_height = padding_top + child_min_height + padding_bottom;

				if (child is IMxGridActor) {
					((IMxGridActor)child).stored_z_position = z_position;
					((IMxGridActor)child).stored_rotation_angle_y = rotation_angle_y;
				} else {
					MxGridActorData data = hash_table.get (child);
					data.z_position = z_position;
					data.rotation_angle_y = rotation_angle_y;
				}

				current_row++; 
			}
		}

		//translate to deformed width
		//param_x [0, width]
		private float translate_width (float param_x, float param_width) {
			if (   (param_x < (adjustment+visible_width))
			    && ((param_x+param_width) > adjustment)) {
				//translate x to visible_center
				double x1 = param_x - visible_center;
				//angle (sector)
				double a_s = 2*Math.asin ((param_width/2)/radius);
				//angle p1
				double a1 = Math.acos (Math.fabs(x1)/radius);
				if (x1 > 0) { //positive x1
					a1 = Math.PI-a1;
				}
				double a2 = a1+a_s;
				//x2
				double x2 = (-1)*radius*(Math.cos (a2));
				return (float)Math.fabs(x1-x2);
			} else {
				return param_width;
			}
		}

		//translate to deformed z_position, rotation_angle_y
		//param_x [0, width]
		private void translate_child (float param_x, float param_width, out float z_position, out float rotation_angle_y) {
			if (   (param_x < (adjustment+visible_width))
			    && ((param_x+param_width) > adjustment)) {
				//translate x to visible_center
				double x1 = param_x - visible_center;
				//z1
				double z1 = Math.sqrt (radius*radius - x1*x1);
				z_position = (float)((-1)*(z1-z_diff));
				//angle (sector)
				double a_s = 2*Math.asin ((param_width/2)/radius);
				//angle p1
				double a1 = Math.acos (Math.fabs(x1)/radius);
				if (x1 > 0) { //positive x1
					a1 = Math.PI-a1;
				}
				double a2 = a1+a_s;
				//x2
				//double x2 = (-1)*radius*(Math.cos (a2));
				//z2
				double z2 = radius*(Math.sin (a2));
				//angle
				rotation_angle_y = (float)rad_to_deg (Math.asin((z2-z1)/param_width));
			} else {
				z_position = 0;
				rotation_angle_y = 0;
			}
		}

		//param_x [0, visible_width]
		public float translate_z (float param_x) {
			//translate x to visible center
			double x1 = param_x - (_visible_width/2);
			//z1
			double z1 = Math.sqrt (radius*radius - x1*x1);
			return (float)((-1)*(z1-z_diff));

		}

		private double rad_to_deg (double rad) {
			return (rad * 180 / Math.PI);
		}

		public float get_padding_top () {
			return extern_get_padding_top (this);
		}

		public float get_padding_right () {
			return extern_get_padding_right (this);
		}

		public float get_padding_bottom () {
			return extern_get_padding_bottom (this);
		}

		public float get_padding_left () {
			return extern_get_padding_left (this);
		}
	}

	private class MxGridActorData : Object {
		public float z_position = 0;
		public float rotation_angle_y = 0;
		public MxGridActorData () {
		}
	}

	public interface IMxGridActor : Clutter.Actor {
		public abstract float stored_z_position { get; set; }
		public abstract float stored_rotation_angle_y { get; set; }
	}

}
